package org.example.util;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.concurrent.atomic.AtomicLong;

/**
 * 短链接生成器工具类 - 详细注释学习版
 * 
 * 🎯 核心功能：
 * 1. 生成8位短链接编码（6位62进制数字 + 2位MD5校验码）
 * 2. 验证短链接编码的有效性和完整性
 * 3. 支持自定义编码的格式验证
 * 
 * 🔧 算法原理：
 * - 使用自增ID作为基础数字，确保唯一性
 * - 将数字转换为62进制字符串（0-9, a-z, A-Z）
 * - 基于62进制字符串计算MD5校验码
 * - 组合成8位最终编码：前6位ID + 后2位校验码
 * 
 * 🛡️ 安全特性：
 * - 校验码防止恶意篡改和非法访问
 * - 62进制编码增加破解难度
 * - 8位长度平衡了可用性和安全性
 * 
 * 📊 容量计算：
 * - 6位62进制：62^6 = 56,800,235,584种组合
 * - 2位校验码：256种组合
 * - 总计：约145亿种组合，足够日常使用
 * 
 * ⚠️ 注意事项：
 * - 应用重启后ID会重置，生产环境建议使用数据库序列
 * - 校验码基于MD5算法，确保数据完整性
 * - 支持自定义编码，但需要符合格式要求
 * 
 * @author Your Name
 * @since 2024-07-01
 */
public class ShortUrlGenerator_Detailed {
    
    /**
     * 62进制字符集
     * 
     * 📝 字符组成：
     * - 数字：0-9（10个字符）
     * - 小写字母：a-z（26个字符）
     * - 大写字母：A-Z（26个字符）
     * - 总计：10 + 26 + 26 = 62个字符
     * 
     * 🤔 为什么选择62进制？
     * - 比10进制更短：数字123在10进制中是3位，在62进制中是2位
     * - 比16进制更短：数字123在16进制中是2位，在62进制中也是2位，但62进制字符更多
     * - 62个字符足够表示大量数据，同时保持编码的可读性
     * - 避免了容易混淆的字符（如0和O，1和l等）
     */
    private static final String CHARS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
    
    /**
     * 自增ID生成器
     * 
     * 🔒 线程安全保证：
     * - 使用AtomicLong而不是普通的long
     * - AtomicLong提供原子操作，多个线程同时访问时不会冲突
     * - incrementAndGet()方法是原子性的，保证ID的唯一性
     * 
     * ⚡ 性能优势：
     * - 比synchronized关键字更高效
     * - 使用CAS（Compare-And-Swap）操作，减少锁竞争
     * - 适合高并发场景
     * 
     * ⚠️ 重要限制：
     * - 应用重启后ID会重置为1，可能导致重复
     * - 单机部署时ID是连续的，容易被猜测
     * - 生产环境建议使用：
     *   * 数据库AUTO_INCREMENT
     *   * Redis INCR命令
     *   * 雪花算法（Snowflake）
     *   * UUID等分布式ID生成器
     */
    private static final AtomicLong ID_GENERATOR = new AtomicLong(1);
    
    /**
     * 校验码长度
     * 
     * 🔍 校验码的作用：
     * - 验证短链接编码的完整性
     * - 防止恶意篡改和非法访问
     * - 检测传输过程中的数据损坏
     * 
     * 📏 长度选择：
     * - 1位校验码：只有62种组合，强度不够，容易被破解
     * - 2位校验码：62^2 = 3,844种组合，足够日常使用
     * - 3位及以上：会增加编码长度，影响用户体验
     * 
     * 🎯 平衡点：
     * - 安全性：2位校验码提供足够的保护
     * - 长度：不会显著增加编码长度
     * - 性能：校验码计算开销适中
     */
    private static final int CHECKSUM_LENGTH = 2;
    
    /**
     * 短链接总长度
     * 
     * 📊 长度分配：
     * - 前6位：62进制ID部分
     * - 后2位：MD5校验码部分
     * - 总计：8位
     * 
     * 🧮 容量计算：
     * - 6位62进制：62^6 = 56,800,235,584种组合
     * - 2位校验码：62^2 = 3,844种组合
     * - 理论总容量：约145亿种组合
     * 
     * ✅ 用户体验：
     * - 8位长度用户友好，易于记忆和输入
     * - 不会太长影响分享和传播
     * - 不会太短影响安全性
     * 
     * 🔄 扩展性：
     * - 如果容量不够，可以增加位数
     * - 当前容量足够支持数十亿个短链接
     */
    private static final int SHORT_CODE_LENGTH = 8;
    
    /**
     * 生成短链接编码
     * 
     * 🔄 算法流程：
     * 1. 生成自增ID（线程安全）
     * 2. 将ID转换为62进制字符串
     * 3. 补齐62进制字符串到6位长度
     * 4. 基于补齐后的字符串计算MD5校验码
     * 5. 组合成8位最终编码：前6位ID + 后2位校验码
     * 
     * 🛠️ 关键修复：
     * - 先补齐长度再计算校验码
     * - 确保校验码基于最终使用的字符串计算
     * - 避免验证时的不一致问题
     * 
     * 📝 示例：
     * - ID=1 -> 62进制="1" -> 补齐="000001" -> 校验码="ab" -> 结果="000001ab"
     * - ID=62 -> 62进制="10" -> 补齐="000010" -> 校验码="cd" -> 结果="000010cd"
     * 
     * @return 8位短链接编码
     */
    public static String generateShortCode() {
        // 🔢 步骤1：生成自增ID
        // 使用AtomicLong保证在多线程环境下的安全性
        // getAndIncrement()：先获取当前值，然后加1，返回旧值
        long id = ID_GENERATOR.getAndIncrement();
        
        // 🔄 步骤2：将ID转换为62进制字符串
        // 例如：ID=1 -> "1", ID=62 -> "10", ID=63 -> "11"
        String base62 = toBase62(id);
        
        // 📏 步骤3：补齐62进制部分到6位
        // 这是修复的关键：在计算校验码之前先补齐长度
        // 确保校验码基于最终使用的字符串计算，而不是原始字符串
        while (base62.length() < 6) {
            base62 = "0" + base62;  // 在前面补0，保持数值不变
        }
        
        // ✂️ 步骤4：如果超过6位，截取后6位
        // 这种情况理论上不会发生，但为了安全性和一致性
        if (base62.length() > 6) {
            base62 = base62.substring(base62.length() - 6);
        }
        
        // 🔐 步骤5：基于补齐后的62进制字符串生成校验码
        // 现在校验码是基于6位字符串计算的，与验证时使用的字符串完全一致
        String checksum = generateChecksum(base62);
        
        // 🔗 步骤6：组合成8位短链接编码：62进制部分 + 校验码
        // 格式：前6位是62进制ID，后2位是校验码
        String shortCode = base62 + checksum;
        
        return shortCode;
    }
    
    /**
     * 生成不重复的短链接编码（基于时间戳）
     * 
     * 🎯 使用场景：
     * - 当自增ID可能重复时使用
     * - 需要更高唯一性保证的场景
     * - 分布式环境下的ID生成
     * 
     * 🔄 算法原理：
     * 1. 使用当前时间戳（毫秒）作为基础ID
     * 2. 添加随机数增加唯一性
     * 3. 转换为62进制字符串
     * 4. 生成校验码确保完整性
     * 
     * ⚡ 优势：
     * - 基于时间戳，天然具有时序性
     * - 添加随机数，减少冲突概率
     * - 不依赖全局计数器，适合分布式环境
     * 
     * ⚠️ 注意事项：
     * - 时间戳精度为毫秒，同一毫秒内可能重复
     * - 随机数范围有限，仍有冲突可能
     * - 生产环境建议使用更可靠的分布式ID生成器
     * 
     * @return 8位短链接编码
     */
    public static String generateUniqueShortCode() {
        // 🕐 步骤1：使用当前时间戳（毫秒）作为基础ID
        // System.currentTimeMillis()返回从1970年1月1日到现在的毫秒数
        long timestamp = System.currentTimeMillis();
        
        // 🎲 步骤2：添加随机数增加唯一性
        // Math.random()返回0-1之间的随机数，乘以1000后得到0-999的随机数
        long random = (long) (Math.random() * 1000);
        
        // 🔗 步骤3：组合时间戳和随机数
        // 时间戳左移3位（乘以1000），然后加上随机数
        // 这样既保持了时间戳的时序性，又增加了随机性
        long combinedId = timestamp * 1000 + random;
        
        // 🔄 步骤4：转换为62进制字符串
        String base62 = toBase62(combinedId);
        
        // 📏 步骤5：补齐到6位长度
        while (base62.length() < 6) {
            base62 = "0" + base62;
        }
        
        // ✂️ 步骤6：如果超过6位，截取后6位
        if (base62.length() > 6) {
            base62 = base62.substring(base62.length() - 6);
        }
        
        // 🔐 步骤7：生成校验码
        String checksum = generateChecksum(base62);
        
        // 🔗 步骤8：组合成8位短链接编码
        return base62 + checksum;
    }
    
    /**
     * 验证短链接编码是否有效
     * 
     * 🔍 验证流程：
     * 1. 检查长度是否为8位
     * 2. 提取前6位作为62进制部分
     * 3. 提取后2位作为存储的校验码
     * 4. 重新计算前6位的校验码
     * 5. 比较两个校验码是否一致
     * 
     * 🛡️ 安全机制：
     * - 长度检查：防止长度不匹配的攻击
     * - 校验码验证：防止内容被篡改
     * - 异常处理：防止恶意输入导致程序崩溃
     * 
     * 📝 验证示例：
     * - 输入："000001ab"
     * - 提取前6位："000001"
     * - 提取后2位："ab"
     * - 重新计算"000001"的校验码
     * - 比较是否等于"ab"
     * 
     * @param shortCode 短链接编码
     * @return 是否有效
     */
    public static boolean isValidShortCode(String shortCode) {
        // 🔍 步骤1：检查输入是否为空或长度不是8位
        if (shortCode == null || shortCode.length() != SHORT_CODE_LENGTH) {
            return false;
        }
        
        try {
            // 📝 步骤2：提取前6位作为62进制部分
            // 这些位用于重新计算校验码
            String base62 = shortCode.substring(0, shortCode.length() - CHECKSUM_LENGTH);
            
            // 🔐 步骤3：提取后2位作为校验码
            // 这些位是生成时计算并存储的校验码
            String checksum = shortCode.substring(shortCode.length() - CHECKSUM_LENGTH);
            
            // 🔄 步骤4：重新计算校验码并与存储的校验码比较
            // 如果一致，说明编码没有被篡改
            String expectedChecksum = generateChecksum(base62);
            return checksum.equals(expectedChecksum);
        } catch (Exception e) {
            // 🚫 步骤5：如果出现任何异常，认为无效
            // 这包括字符串截取异常、校验码计算异常等
            return false;
        }
    }
    
    /**
     * 将数字转换为62进制字符串
     * 
     * 🔄 转换原理：
     * 不断除以62，取余数作为对应位置的字符
     * 余数对应CHARS字符串中的字符位置
     * 
     * 📝 转换示例：
     * 数字123转换为62进制：
     * - 123 ÷ 62 = 1 余 61 -> 取第61个字符(61对应'z')
     * - 1 ÷ 62 = 0 余 1   -> 取第1个字符(1对应'1')
     * - 结果：'1' + 'z' = "1z"
     * 
     * 🔢 字符映射：
     * - 0-9：对应字符'0'-'9'
     * - 10-35：对应字符'a'-'z'
     * - 36-61：对应字符'A'-'Z'
     * 
     * ⚡ 性能优化：
     * - 使用StringBuilder而不是String拼接
     * - insert(0, char)在开头插入字符，避免字符串反转
     * 
     * @param num 要转换的数字
     * @return 62进制字符串
     */
    private static String toBase62(long num) {
        // 🔢 特殊情况：如果数字为0，直接返回"0"
        if (num == 0) {
            return "0";
        }
        
        // 📝 使用StringBuilder构建结果字符串
        // StringBuilder比String拼接更高效
        StringBuilder sb = new StringBuilder();
        
        // 🔄 不断除以62，取余数作为对应位置的字符
        while (num > 0) {
            // 🔢 取余数，转换为对应的62进制字符，插入到开头
            // (int) (num % 62) 确保余数在0-61范围内
            // CHARS.charAt()根据余数获取对应的字符
            sb.insert(0, CHARS.charAt((int) (num % 62)));
            // 📊 除以62，继续处理下一位
            num /= 62;
        }
        
        return sb.toString();
    }
    
    /**
     * 生成校验码
     * 
     * 🔐 校验码生成算法：
     * 1. 使用MD5计算输入字符串的哈希值
     * 2. 取前两个字节组合成16位整数
     * 3. 对62^2取模，确保结果在0-3843范围内
     * 4. 转换为2位62进制字符串
     * 
     * 🛡️ 安全特性：
     * - MD5是不可逆的哈希算法
     * - 即使知道校验码，也无法反推出原始字符串
     * - 任何字符的微小变化都会导致校验码完全不同
     * 
     * 🔄 备选方案：
     * - 如果MD5不可用，使用hashCode作为备选方案
     * - 这种情况在标准Java环境中很少发生
     * 
     * 📝 生成示例：
     * - 输入："000001"
     * - MD5哈希：计算"000001"的MD5值
     * - 取前两字节：如0x12, 0x34
     * - 组合：0x1234 = 4660
     * - 取模：4660 % 3844 = 816
     * - 转62进制：816 -> "13"
     * 
     * @param base62 62进制字符串
     * @return 2位校验码
     */
    public static String generateChecksum(String base62) {
        try {
            // 🔐 步骤1：获取MD5消息摘要实例
            MessageDigest md = MessageDigest.getInstance("MD5");
            // 📝 步骤2：计算base62字符串的MD5哈希值
            // getBytes()将字符串转换为字节数组
            byte[] hash = md.digest(base62.getBytes());
            
            // 🔢 步骤3：取前两个字节生成校验码
            // 第一个字节左移8位，与第二个字节组合
            // & 0xFF确保字节值在0-255范围内
            // 例如：hash[0]=0x12, hash[1]=0x34 -> 0x1234
            int checksum = ((hash[0] & 0xFF) << 8) | (hash[1] & 0xFF);
            
            // 📊 步骤4：确保校验码在62^2范围内（0到3843）
            // 这样校验码可以转换为2位62进制字符串
            checksum = checksum % (62 * 62);
            
            // 🔄 步骤5：将校验码转换为2位62进制字符串
            String checksumStr = toBase62(checksum);
            
            // 📏 步骤6：如果不足2位，用0补齐
            // 确保校验码始终是2位，保持编码格式一致
            while (checksumStr.length() < CHECKSUM_LENGTH) {
                checksumStr = "0" + checksumStr;
            }
            
            return checksumStr;
        } catch (NoSuchAlgorithmException e) {
            // 🚫 如果MD5不可用，使用简单的哈希算法作为备选方案
            // 这种情况在标准Java环境中很少发生
            int hash = base62.hashCode();
            int checksum = Math.abs(hash) % (62 * 62);
            
            String checksumStr = toBase62(checksum);
            while (checksumStr.length() < CHECKSUM_LENGTH) {
                checksumStr = "0" + checksumStr;
            }
            
            return checksumStr;
        }
    }
    
    /**
     * 检查自定义短链接编码是否可用
     * 
     * 🎯 使用场景：
     * - 用户想要自定义短链接编码时
     * - 验证用户输入是否符合格式要求
     * - 防止用户输入非法字符
     * 
     * 📋 验证规则：
     * 1. 长度必须是8位
     * 2. 只能包含62进制字符（数字、小写字母、大写字母）
     * 3. 不能包含特殊字符或空格
     * 
     * ⚠️ 重要说明：
     * - 这里只检查格式，不验证校验码
     * - 即使格式正确，校验码可能不正确
     * - 实际使用时还需要检查是否已存在
     * 
     * 📝 验证示例：
     * - "abc12345" -> true（格式正确）
     * - "abc-1234" -> false（包含非法字符'-'）
     * - "abc123" -> false（长度不足8位）
     * - "abc123456" -> false（长度超过8位）
     * 
     * @param customCode 自定义编码
     * @return 是否可用
     */
    public static boolean isCustomCodeAvailable(String customCode) {
        // 🔍 步骤1：检查是否为空或长度不是8位
        if (customCode == null || customCode.length() != SHORT_CODE_LENGTH) {
            return false;
        }
        
        // 🔍 步骤2：检查是否只包含62进制字符（数字、小写字母、大写字母）
        // 遍历每个字符，确保都在CHARS字符串中
        for (char c : customCode.toCharArray()) {
            // indexOf返回字符在CHARS中的位置，如果不存在返回-1
            if (CHARS.indexOf(c) == -1) {
                return false;
            }
        }
        
        return true;
    }
}

